#include "BBS2chProxyPoster.h"
#include <vector>
#include <map>
#include <sstream>
#include <algorithm>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#ifdef _WIN32
#include <windows.h>
#endif
#ifdef USE_LUA
#include <lua.hpp>
#endif
#include "BBS2chProxyAuth.h"
#include "BBS2chProxyKeyManager.h"
#include "hmac.h"
#include "stringEncodingConverter.h"
#include "utils.h"
#include "parson/parson.h"

extern char *user_agent;
extern char *appKey;
extern char *hmacKey;
extern unsigned int api_mode;
extern BBS2chProxyHttpHeaders bbscgi_headers;
extern std::vector<std::string> bbscgi_postorder;
extern unsigned int bbscgi_utf8;
extern char *lua_script;
extern int allow_chunked;
extern int manage_bbscgi_cookies;
extern unsigned int curl_version_number;
extern int bbscgi_confirmation;
extern int bbscgi_fix_timestamp;

extern void log_printf(int level, const char *format ...);

BBS2chProxy5chPoster::ConfirmationManager BBS2chProxy5chPoster::confirmationManager;

static size_t header_callback_bbscgi(char *buffer, size_t size, size_t nitems, void *userdata)
{
	BBS2chProxy5chPoster *poster = static_cast<BBS2chProxy5chPoster *>(userdata);
	BBS2chProxyConnection *conn = poster->connectionDelegate;
	if(poster->_status > 255) return size*nitems;
	else if(poster->_status == -1) {
		if(!memcmp(buffer,"\r\n",2)) {
			poster->_status = 0;
		}
		return size*nitems;
	}
	PBBS2chProxyHttpHeaderEntry parsedHeader = BBS2chProxyHttpHeaders::parse(buffer, size*nitems);
	if (parsedHeader) { // Looks like a header
		const std::string &headerName = parsedHeader->getLowercasedName();
		//fprintf(stderr, "%s\n", parsedHeader->getFull().c_str());
		if (headerName == "connection") {
			poster->_responseHeaders.append("Connection: Close\r\n");
			return size*nitems;
		}
		else if (headerName == "transfer-encoding") {
			if (parsedHeader->contains("chunked")) {
				if (allow_chunked && !conn->isClientHttp1_0) {
					poster->_isResponseChunked = true;
					poster->_responseHeaders.append(buffer, size*nitems);
				}
				return size*nitems;
			}
		}
		else if (headerName == "set-cookie") {
			poster->_hasSetCookie = true;
			if (conn->force5ch) {
				std::string value = parsedHeader->getFull(true);
				std::string lowercasedValue = value;
				std::transform(lowercasedValue.begin(), lowercasedValue.end(), lowercasedValue.begin(), tolower);
				size_t start = lowercasedValue.find("domain=");
				if (start != std::string::npos) {
					start += 7;
					size_t end = value.find(";", start);
					size_t pos = value.find("5ch.net", start);
					if (pos != std::string::npos && (end == std::string::npos || pos < end)) {
						if (pos == start || value[pos-1] == '.') {
							value[pos] = '2';
							poster->_responseHeaders.append(value);
							return size*nitems;
						}
					}
				}
			}
		}
		else if (headerName == "x-chx-error") {
			const std::string &headerValue = parsedHeader->getValue();
			const char *ptr = headerValue.c_str();
			log_printf(0, "Posting to bbs.cgi has been cancelled. Reason: %s\n", ptr);
			if (*ptr == 'E') {
				int code = atoi(ptr+1);
				if ((code >= 3310 && code <= 3311) || /* inconsistent with User-Agent or something? */
					(code >= 3320 && code <= 3324) || /* key expiration? */
					(code >= 3390 && code <= 3392))   /* 3390: BAN, 3391-3392: key expiration? */
				{
					BBS2chProxyConnection::keyManager.setKey("", poster->_monaKeyForRequest, poster->_userAgentForRequest, code);
					if (poster->_isFirstRun) {
						poster->_status = 1;
					}
				}
				else if (code == 3000 || code == 3100 || code == 4000) { // MonaTicket is invalid
					std::string setCookie = "Set-Cookie: MonaTicket=; expires=Thu, 01-Jan-1970 00:00:00 GMT; path=/; domain=.";
					size_t pos = setCookie.size();
					if (conn->force5ch) setCookie += "2";
					else setCookie += "5";
					setCookie += "ch.net\r\n";
					poster->_responseHeaders.append(setCookie);
#if LIBCURL_VERSION_NUM >= 0x070e01 /* curl 7.14.1 or later */
					if (poster->_manageCookies) {
						setCookie[pos] = '5';
						curl_easy_setopt(conn->curl, CURLOPT_COOKIELIST, setCookie.c_str());
						poster->_hasSetCookie = true;
					}
#endif
				}
			} else if (!strncmp(ptr, "0000 Confirmation", 17) && poster->_confirmationHandlerMode > 0) {
				if (poster->_confirmationHandlerMode == 2 && poster->_isFirstRun) poster->_status = 3;
				else if (!poster->_hasAcceptEncoding) poster->_status = 4;
			} else if (poster->_manageCookies) {
				int code = atoi(ptr);
				if ((code == 1932) && poster->_isFirstRun) { // was 1930, 1931
					poster->_status = 2;
				}
			}
		}
		else if (headerName == "x-monakey") {
			BBS2chProxyConnection::keyManager.setKey(parsedHeader->getValue(), poster->_monaKeyForRequest, poster->_userAgentForRequest, 0);
		}
		else if (poster->_useTransparentContentEncoding && headerName == "content-encoding") {
			return size*nitems;
		}
		else if (poster->_useTransparentContentEncoding && headerName == "content-length") {
			return size*nitems;
		}
		poster->_responseHeaders.append(buffer, size*nitems);
		return size*nitems;
	}
	else {
		if (!strncasecmp("HTTP/", buffer, 5)) {
			const char *ptr = buffer + 5;
			const char *end = buffer + size*nitems;
			while (ptr < end && *ptr != ' ') ptr++;
			while (ptr < end && *ptr == ' ') ptr++;
			if (ptr < end) {
				int code = atoi(ptr);
				if (code == 100) {
					poster->_status = -1;
					return size*nitems;
				}
			}
		}
		poster->_responseHeaders.append(buffer, size*nitems);
		if (!memcmp(buffer, "\r\n", 2)) {
			if (poster->_status == 0 || poster->_status == 4) {
				conn->socketToClient->write(poster->_responseHeaders.data(), poster->_responseHeaders.size());
				if (poster->_status == 0) poster->_status = 256;
				else poster->_status = 513;
			} else if (poster->_status == 3) {
				poster->_status = 512;
			}
			else return 0;
		}
		return size*nitems;
	}
}

static size_t write_callback_bbscgi(char *buffer, size_t size, size_t nitems, void *userdata)
{
	BBS2chProxy5chPoster *poster = static_cast<BBS2chProxy5chPoster *>(userdata);
	if (poster->_status >= 512) {
		poster->_responseBody.insert(poster->_responseBody.end(), buffer, buffer+size*nitems);
		if (poster->_status == 512) return size*nitems;
	}
	BBS2chProxyConnection *conn = poster->connectionDelegate;
	if(poster->_isResponseChunked) {
		char buf[64];
		snprintf(buf, 64, "%lx\r\n", size*nitems);
		conn->socketToClient->write(buf, strlen(buf));
	}
	size_t ret = conn->socketToClient->write(buffer, size*nitems);
	if(poster->_isResponseChunked) conn->socketToClient->writeString("\r\n");
	return ret;
}

static bool isValidAsUTF8(const char *input, size_t inputLength) {
	for (int i=0; i<inputLength; i++) {
		unsigned char c1 = input[i];
		if (c1 < 0x80) continue;
		else if (c1 >= 0xc2 && c1 <= 0xdf) {
			if (i >= inputLength - 1) return false;
			unsigned char c2 = input[++i];
			if (c2 < 0x80 || c2 > 0xbf) return false;
			unsigned int unicode = c2 & 0xf;
			unicode |= (((c2 >> 4) & 0x3) | ((c1 & 0x3) << 2)) << 4;
			unicode |= ((c1 >> 2) & 0x7) << 8;
			if (unicode < 0x80 || unicode > 0x7ff) return false;
		}
		else if (c1 >= 0xe0 && c1 <= 0xef) {
			if (i >= inputLength - 2) return false;
			unsigned char c2 = input[++i];
			if (c2 < 0x80 || c2 > 0xbf) return false;
			unsigned char c3 = input[++i];
			if (c3 < 0x80 || c3 > 0xbf) return false;
			unsigned int unicode = c3 & 0xf;
			unicode |= (((c3 >> 4) & 0x3) | ((c2 & 0x3) << 2)) << 4;
			unicode |= ((c2 >> 2) & 0xf) << 8;
			unicode |= (c1 & 0xf) << 12;
			if (unicode < 0x800 || unicode > 0xffff) return false;
			else if (unicode >= 0xd800 && unicode <= 0xdfff) return false; /* for surrogate pairs */
		}
		else if (c1 >= 0xf0 && c1 <= 0xf7) {
			if (i >= inputLength - 3) return false;
			unsigned char c2 = input[++i];
			if (c2 < 0x80 || c2 > 0xbf) return false;
			unsigned char c3 = input[++i];
			if (c3 < 0x80 || c3 > 0xbf) return false;
			unsigned char c4 = input[++i];
			if (c4 < 0x80 || c4 > 0xbf) return false;
			unsigned int unicode = c4 & 0xf;
			unicode |= (((c4 >> 4) & 0x3) | ((c3 & 0x3) << 2)) << 4;
			unicode |= ((c3 >> 2) & 0xf) << 8;
			unicode |= (c2 & 0xf) << 12;
			unicode |= (((c2 >> 4) & 0x3) | ((c1 & 0x3) << 2)) << 16;
			unicode |= ((c1 >> 2) & 0x1) << 20;
			if (unicode < 0x10000 || unicode > 0x10ffff) return false;
		}
		else return false;
	}
	return true;
}

static void appendPostSignature(BBS2chProxyFormData &body, const std::string &userAgent, const std::string &monaKey, BBS2chProxyHttpHeaders &headers, bool forTalk)
{
	char nonce[32];
	std::string message;
	if (!forTalk) {
		snprintf(nonce, 32, "%.3f", getCurrentTime());
		message.append(body["bbs"]);
		message.append("<>");
		message.append(body["key"]);
		message.append("<>");
		message.append(body["time"]);
		message.append("<>");
		message.append(body["FROM"]);
		message.append("<>");
		message.append(body["mail"]);
		message.append("<>");
		message.append(body["MESSAGE"]);
		message.append("<>");
		message.append(body["subject"]);
		message.append("<>");
		message.append(userAgent);
		message.append("<>");
		message.append(monaKey);
		message.append("<>");
		message.append("<>");
		message.append(nonce);
	} else {
		message.append(body["bbs"]);
		message.append("<>");
		message.append(body["key"]);
		message.append("<>");
		message.append(body["subject"]);
		message.append("<>");
		message.append(body["MESSAGE"]);
		message.append("<>");
		message.append(body["time"]);
		message.append("<>");
		message.append(body["sid"]);
		message.append("<>");
	}
	unsigned char digest[32];
	char digestStr[65];
	static const char *table = "0123456789abcdef";
	proxy2ch_HMAC_SHA256(hmacKey, strlen(hmacKey), message.data(), message.length(), digest);
	for (int i=0; i<32; i++) {
		unsigned char c = digest[i];
		unsigned char upper = (c >> 4) & 0xf;
		unsigned char lower = c & 0xf;
		digestStr[i*2] = table[upper];
		digestStr[i*2+1] = table[lower];
	}
	digestStr[64] = 0;
	if (!forTalk) {
		headers.set("X-APIKey", appKey);
		log_printf(1, "Appended header \"X-APIKey: %s\"\n", appKey);
		headers.set("X-PostSig", digestStr);
		log_printf(1, "Appended header \"X-PostSig: %s\"\n", digestStr);
		headers.set("X-PostNonce", nonce);
		log_printf(1, "Appended header \"X-PostNonce: %s\"\n", nonce);
		headers.set("X-MonaKey", monaKey);
		log_printf(1, "Appended header \"X-MonaKey: %s\"\n", monaKey.c_str());
	} else {
		headers.set("X-Write-Token", digestStr);
		log_printf(1, "Appended header \"X-Write-Token: %s\"\n", digestStr);
		if (monaKey != "00000000-0000-0000-0000-000000000000") {
			headers.set("X-Write-Key", monaKey);
			log_printf(1, "Appended header \"X-Write-Key: %s\"\n", monaKey.c_str());
		}
	}
}

static bool convertBodyToUTF8(BBS2chProxyFormData &body)
{
	bool shouldConvertToUTF8 = true;
	bool shouldCheckWholeBody = true;
	const std::string &submit = body.get("submit");
	if (body.getEncoded("submit").size() != submit.size()) {
		if (isValidAsUTF8(submit.data(), submit.size())) {
			shouldConvertToUTF8 = false;
		}
		shouldCheckWholeBody = false;
	}
	if (shouldCheckWholeBody) {
		shouldConvertToUTF8 = false;
		for (BBS2chProxyFormData::iterator it = body.begin(); it != body.end(); ++it) {
			if (it->second.empty()) continue;
			if (!isValidAsUTF8(it->second.get().data(), it->second.get().size())) {
				shouldConvertToUTF8 = true;
				break;
			}
		}
	}
	if (shouldConvertToUTF8) {
		for (BBS2chProxyFormData::iterator it = body.begin(); it != body.end(); ++it) {
			if (it->second.empty()) continue;
			char *converted = convertShiftJISToUTF8(it->second.get().data(), it->second.get().size());
			if (converted) {
				it->second = std::string(converted);
				free(converted);
			}
		}
	}
	return shouldConvertToUTF8;
}

#if LIBCURL_VERSION_NUM >= 0x070e01 /* curl 7.14.1 or later */
static void appendCurlCookiesToJar(CURL *curl, BBS2chProxyKeyManager::CookieJar &jar)
{
	struct curl_slist *cookies = NULL;
	if (!curl_easy_getinfo(curl, CURLINFO_COOKIELIST, &cookies) && cookies) {
		struct curl_slist *each = cookies;
		jar.lock();
		jar.clear();
		while (each) {
			BBS2chProxyKeyManager::Cookie cookie(each->data);
			jar.set(cookie);
			each = each->next;
		}
		jar.unlock();
		BBS2chProxyConnection::keyManager.flushCookies();
		curl_slist_free_all(cookies);
	}
}
#endif

static int appendUnknownFieldFromResponseHtml(const std::vector<char> &responseBody, BBS2chProxyFormData &srcBody, BBS2chProxyFormData &dstBody)
{
	int appended = 0;
	const char *ptr = &responseBody.front();
	ptr = strstr(ptr, "<form method=\"POST\"");
	const char *end = ptr ? strstr(ptr, "</form>") : NULL;
	while (ptr && end && ptr < end) {
		ptr = strstr(ptr, "<input ");
		if (!ptr) break;
		ptr += 7;
		const char *start = ptr;
		const char *last = strchr(ptr, '>');
		if (!last) break;
		while (ptr < last) {
			if (!strncmp(ptr, "name=", 5)) break;
			ptr++;
		}
		ptr += 5;
		if (ptr >= last) continue;
		char endChar = ' ';
		if (*ptr == '"' || *ptr == '\'') {
			endChar = *ptr;
			ptr++;
		}
		std::string field;
		while (*ptr != endChar && *ptr != '>') field += *ptr++;
		if (srcBody.has(field)) continue;
		ptr = start;
		while (ptr < last) {
			if (!strncmp(ptr, "value=", 6)) break;
			ptr++;
		}
		ptr += 6;
		if (ptr >= last) continue;
		endChar = ' ';
		if (*ptr == '"' || *ptr == '\'') {
			endChar = *ptr;
			ptr++;
		}
		std::string value;
		while (*ptr != endChar && *ptr != '>') value += *ptr++;
		log_printf(1, "Found token-like field \"%s=%s\" in the confirmation form\n", field.c_str(), value.c_str());
		dstBody.append(field, value);
		appended++;
	}
	return appended;
}

#ifdef USE_LUA
extern "C" {
	static int lua_hmacSHA256(lua_State *l)
	{
		static const char *table = "0123456789abcdef";
		size_t keyLength, dataLength;
		const char *key = luaL_checklstring(l, 1, &keyLength);
		const char *data = luaL_checklstring(l, 2, &dataLength);
		if (!key || !data) return 0;
		unsigned char digest[32];
		char digestStr[65];
		proxy2ch_HMAC_SHA256(key, keyLength, data, dataLength, digest);
		for (int i=0; i<32; i++) {
			unsigned char c = digest[i];
			unsigned char upper = (c >> 4) & 0xf;
			unsigned char lower = c & 0xf;
			digestStr[i*2] = table[upper];
			digestStr[i*2+1] = table[lower];
		}
		digestStr[64] = 0;
		lua_pushstring(l, digestStr);
		return 1;
	}
	static int lua_decodeURIComponent(lua_State *l)
	{
		size_t length;
		const char *input = luaL_checklstring(l, 1, &length);
		if (!input) return 0;
		bool decodePlus = true;
		if (!lua_isnoneornil(l, 2)) {
			decodePlus = (lua_toboolean(l, 2));
		}

		std::string output = BBS2chProxyFormData::decodeURIComponent(input, length, decodePlus);
		lua_pushstring(l, output.c_str());
		return 1;
	}
	static int lua_encodeURIComponent(lua_State *l)
	{
		size_t length;
		const char *input = luaL_checklstring(l, 1, &length);
		if (!input) return 0;
		bool spaceAsPlus = true;
		if (!lua_isnoneornil(l, 2)) {
			spaceAsPlus = (lua_toboolean(l, 2));
		}

		std::string output = BBS2chProxyFormData::encodeURIComponent(input, length, spaceAsPlus);
		lua_pushstring(l, output.c_str());
		return 1;
	}
	static int lua_convertShiftJISToUTF8(lua_State *l)
	{
		size_t length;
		const char *input = luaL_checklstring(l, 1, &length);
		if (!input) return 0;
		if (length > 0) {
			char *output = convertShiftJISToUTF8(input, length);
			if (!output) lua_pushnil(l);
			else {
				lua_pushstring(l, output);
				free(output);
			}
		}
		else lua_pushstring(l, "");
		return 1;
	}
	static int lua_isExpiredKey(lua_State *l)
	{
		size_t length;
		const char *input = luaL_checklstring(l, 1, &length);
		if (!input) return 0;
		if (BBS2chProxyConnection::keyManager.isExpired(input)) {
			lua_pushboolean(l, 1);
		}
		else lua_pushboolean(l, 0);
		return 1;
	}
	static int lua_isValidAsUTF8(lua_State *l)
	{
		size_t length;
		const char *input = luaL_checklstring(l, 1, &length);
		if (!input) return 0;
		lua_pushboolean(l, isValidAsUTF8(input, length));
		return 1;
	}
	static int lua_getMonaKey(lua_State *l)
	{
		size_t length;
		const char *input = luaL_checklstring(l, 1, &length);
		if (!input) return 0;
		const std::string &key = BBS2chProxyConnection::keyManager.getKey(input);
		lua_pushstring(l, key.c_str());
		return 1;
	}
	static int lua_getSID(lua_State *l)
	{
		const std::string &sid = BBS2chProxyConnection::auth.getSID();
		lua_pushstring(l, sid.c_str());
		return 1;
	}
}
#endif

IBBS2chProxyPoster::IBBS2chProxyPoster(BBS2chProxyURL &url, BBS2chProxyHttpHeaders &headers, BBS2chProxyFormData &body, BBS2chProxyConnection *delegate)
	: _requestHeaders(headers), _requestBody(body), _verbose(0), connectionDelegate(delegate), _manageCookies(false)
{
	prepareHeadersAndBody(url);
}

BBS2chProxy5chPoster::BBS2chProxy5chPoster(BBS2chProxyURL &url, BBS2chProxyHttpHeaders &headers, BBS2chProxyFormData &body, BBS2chProxyConnection *delegate)
	: IBBS2chProxyPoster(url, headers, body, delegate), _isFirstRun(true), _status(0), _isResponseChunked(false), _hasSetCookie(false)
{
	_url = url.absoluteString();
	_confirmationHandlerMode = url.isKindOfHost("5ch.net") ? bbscgi_confirmation : 0;
}

BBS2chProxyTalkPoster::BBS2chProxyTalkPoster(BBS2chProxyURL &url, BBS2chProxyHttpHeaders &headers, BBS2chProxyFormData &body, BBS2chProxyConnection *delegate)
	: IBBS2chProxyPoster(url, headers, body, delegate)
{
	_url = "https://api.talk-platform.com/v1/bbs.cgi";
	_requestBody.remove("sid");
	_requestHeaders.remove("Accept-Encoding");
}

BBS2chProxyTalkTo5chPoster::BBS2chProxyTalkTo5chPoster(BBS2chProxyURL &url, BBS2chProxyHttpHeaders &headers, BBS2chProxyFormData &body, BBS2chProxyConnection *delegate)
	: BBS2chProxy5chPoster(url, headers, body, delegate)
{
	_confirmationHandlerMode = url.isKindOfHost("5ch.net") ? 2 : 0;
	_requestHeaders.remove("Accept-Encoding");
}

void IBBS2chProxyPoster::prepareHeadersAndBody(const BBS2chProxyURL &url)
{
	_host = _requestHeaders.get("Host");
	_board = _requestBody.get("bbs");
	_thread = _requestBody.get("key");
	if (!bbscgi_postorder.empty()) {
		_requestBody.reorder(bbscgi_postorder);
		log_printf(1, "Reordered request body is: %s\n", _requestBody.toString().c_str());
	}
	_requestHeaders.remove("Host");
	if (user_agent) _requestHeaders.set("User-Agent", user_agent);
	if (url.isFamilyOf5chNet() && _requestHeaders.has("Referer")) {
		BBS2chProxyURL referrer(_requestHeaders.get("Referer").c_str());
		if (url.getScheme() != referrer.getScheme()) {
			referrer.setScheme(url.getScheme());
			_requestHeaders.set("Referer", referrer.absoluteString());
		}
	}
	if (!bbscgi_headers.empty()) {
		for (std::map<std::string, PBBS2chProxyHttpHeaderEntry>::iterator it = bbscgi_headers.getMap().begin(); it != bbscgi_headers.getMap().end(); it++) {
			/* we create a copy of entry here, because the original entry shouldn't be modified */
			PBBS2chProxyHttpHeaderEntry entry(new BBS2chProxyHttpHeaderEntry(*it->second.get()));
			if (!_host.empty()) {
				entry->replaceValue("%HOST%", _host);
			}
			if (!_board.empty()) {
				entry->replaceValue("%BOARD%", _board);
			}
			if (!_thread.empty()) {
				entry->replaceValue("%THREAD%", _thread);
			}
			_requestHeaders.set(entry);
			log_printf(1, "Appended custom header \"%s\"\n", entry->getFull().c_str());
		}
	}
	if (bbscgi_fix_timestamp && _requestBody.has("time")) {
		std::ostringstream ss;
		ss << (time(NULL) - 10);
		_requestBody.set("time", ss.str());
		log_printf(1, "Updated post timestamp to %s\n", ss.str().c_str());
	}
}

curl_slist* IBBS2chProxyPoster::prepareCurlHandle(BBS2chProxyHttpHeaders &headers, BBS2chProxyFormData &body, curl_slist* headersForCurl)
{
	CURL *curl = connectionDelegate->curl;
#if LIBCURL_VERSION_NUM >= 0x070e01 /* curl 7.14.1 or later */
	if (_manageCookies) {
		log_printf(1, "Cookies are managed by proxy2ch, most of existing \"Cookie: \" headers are ignored.\n");
		curl_easy_setopt(curl, CURLOPT_COOKIEFILE, ""); //enable cookie engine explicitly
		curl_easy_setopt(curl, CURLOPT_COOKIELIST, "ALL");  //erase all cookies explicitly
		bool hasBeCookieInJar = false;
		bool hasUpliftSidCookieInJar = false;
		BBS2chProxyKeyManager::CookieJar &jar = BBS2chProxyConnection::keyManager.getCookieJar(_userAgentForRequest, true);
		jar.lock();
		std::vector<BBS2chProxyKeyManager::Cookie> &list = jar.getList();
		for (std::vector<BBS2chProxyKeyManager::Cookie>::iterator it = list.begin(); it != list.end(); ++it) {
			if (!it->isExpired()) {
				curl_easy_setopt(curl, CURLOPT_COOKIELIST, it->valueInNetscapeFormat().c_str());
				if (!hasBeCookieInJar && (it->name == "DMDM" || it->name == "MDMD") && it->domain == ".5ch.net") {
					if (it->domain.size() <= _host.size() && _host.find(it->domain, _host.size()-it->domain.size()) != std::string::npos) {
						hasBeCookieInJar = true;
					}
				}
				if (!hasUpliftSidCookieInJar && it->name == "sid" && (it->domain == ".5ch.net" || it->domain == ".bbspink.com")) {
					if (it->domain.size() <= _host.size() && _host.find(it->domain, _host.size()-it->domain.size()) != std::string::npos) {
						hasUpliftSidCookieInJar = true;
					}
				}
			}
		}
		jar.unlock();
		if (headers.has("Cookie")) {
			const std::string &value = headers.getEntry("Cookie")->getValue();
			std::vector<std::string> list;
			size_t offset = 0;
			while (1) {
				size_t pos = value.find("; ", offset);
				if (pos == std::string::npos) {
					list.push_back(value.substr(offset));
					break;
				}
				list.push_back(value.substr(offset, pos - offset));
				offset = pos + 2;
			}
			std::string newCookie;
			for (std::vector<std::string>::iterator it = list.begin(); it != list.end(); ++it) {
				size_t pos = it->find('=');
				if (pos == std::string::npos) continue;
				std::string name = it->substr(0, pos);
				if ((!hasUpliftSidCookieInJar && name == "sid") || (!hasBeCookieInJar && (name == "DMDM" || name == "MDMD"))) {
					if (newCookie.size()) newCookie += "; ";
					newCookie += *it;
				}
			}
			if (newCookie.size()) curl_easy_setopt(curl, CURLOPT_COOKIE, newCookie.c_str());
			headers.remove("Cookie");
		}
		if (hasUpliftSidCookieInJar) {
			headers.remove("X-Ronin-Sid");
			body.remove("sid");
		}
	}
#endif
	headersForCurl = headers.appendToCurlSlist(headersForCurl);
	if (!headers.has("Expect")) headersForCurl = curl_slist_append(headersForCurl, "Expect:");
	if (!headers.has("Accept")) headersForCurl = curl_slist_append(headersForCurl, "Accept:");
	configureCurlHandle(curl);
	curl_easy_setopt(curl, CURLOPT_URL, _url.c_str());
	curl_easy_setopt(curl, CURLOPT_POST, 1L);
	curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.toString().c_str());
	curl_easy_setopt(curl, CURLOPT_VERBOSE, _verbose);
	curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headersForCurl);
	if (!_nic.empty()) curl_easy_setopt(curl, CURLOPT_INTERFACE, _nic.c_str());
	if (!_forceProxy.empty()) {
		curl_easy_setopt(curl, CURLOPT_PROXYPORT, 0L);
		curl_easy_setopt(curl, CURLOPT_PROXYTYPE, CURLPROXY_HTTP);
		curl_easy_setopt(curl, CURLOPT_PROXY, _forceProxy.c_str());
	}
	return headersForCurl;
}

void IBBS2chProxyPoster::runLuaScript(BBS2chProxyHttpHeaders &headers, BBS2chProxyFormData &body)
{
#ifdef USE_LUA
	lua_State* l = luaL_newstate();
	luaL_openlibs(l);
	if (luaL_loadfile(l, lua_script) != LUA_OK) {
		log_printf(0, "Lua: Failed to open script %s:\n  %s\n", lua_script, lua_tostring(l, -1));
		goto lua_end;
	}
	lua_newtable(l);
	lua_pushcfunction(l, lua_hmacSHA256);
	lua_setfield(l, -2, "hmacSHA256");
	lua_pushcfunction(l, lua_decodeURIComponent);
	lua_setfield(l, -2, "decodeURIComponent");
	lua_pushcfunction(l, lua_encodeURIComponent);
	lua_setfield(l, -2, "encodeURIComponent");
	lua_pushcfunction(l, lua_convertShiftJISToUTF8);
	lua_setfield(l, -2, "convertShiftJISToUTF8");
	lua_pushcfunction(l, lua_isExpiredKey);
	lua_setfield(l, -2, "isExpiredKey");
	lua_pushcfunction(l, lua_isValidAsUTF8);
	lua_setfield(l, -2, "isValidAsUTF8");
	lua_pushcfunction(l, lua_getMonaKey);
	lua_setfield(l, -2, "getMonaKey");
	lua_pushcfunction(l, lua_getSID);
	lua_setfield(l, -2, "getSID");
	lua_pushstring(l, BBS2chProxyConnection::keyManager.getKey().c_str());
	lua_setfield(l, -2, "monaKey");
	lua_pushinteger(l, connectionDelegate->serverPort);
	lua_setfield(l, -2, "port");
	lua_setglobal(l, "proxy2ch");
	BBS2chProxyHttpHeaders::getClassDefinitionForLua(l);
	lua_setglobal(l, "HttpHeaders");
	if (lua_pcall(l, 0, 0, 0) != LUA_OK) {
		log_printf(0, "Lua: Failed to run script %s:\n  %s\n", lua_script, lua_tostring(l, -1));
		goto lua_end;
	}
	lua_getglobal(l, "willSendRequestToBbsCgi");
	if (!lua_isfunction(l, -1)) {
		log_printf(0, "Lua: willSendRequestToBbsCgi function does not exist in the script\n");
		goto lua_end;
	}
	lua_newtable(l);
	headers.getUserdataForLua(l);
	lua_setfield(l, -2, "headers");
	lua_pushstring(l, body.toString().c_str());
	lua_setfield(l, -2, "body");
	lua_pushstring(l, _host.c_str());
	lua_pushstring(l, _board.c_str());
	lua_pushstring(l, _thread.c_str());
	if (lua_pcall(l, 4, 1, 0) != LUA_OK) {
		log_printf(0, "Lua: Failed to call willSendRequestToBbsCgi function:\n  %s\n", lua_tostring(l, -1));
		goto lua_end;
	}
	if (!lua_istable(l, -1)) {
		log_printf(0, "Lua: A return type of willSendRequestToBbsCgi function should be a table\n");
		goto lua_end;
	}
	lua_pushstring(l, "body");
	lua_rawget(l, -2);
	if (lua_isstring(l, -1)) {
		size_t length;
		const char *newBody = lua_tolstring(l, -1, &length);
		body = BBS2chProxyFormData(newBody, length);
		log_printf(1, "Lua: Set request body \"%s\"\n", newBody);
	}
	lua_pop(l, 1);
	lua_pushstring(l, "headers");
	lua_rawget(l, -2);
	if (lua_istable(l, -1)) {
		headers = BBS2chProxyHttpHeaders();
		lua_pushnil(l);
		while (lua_next(l, -2)) {
			if (lua_isstring(l, -1) && lua_isstring(l, -2)) {
				const char *name = lua_tostring(l, -2);
				const char *value = lua_tostring(l, -1);
				headers.add(name, value);
				log_printf(1, "Lua: Set request header \"%s: %s\"\n", name, value);
			}
			lua_pop(l, 1);
		}
	}
	else if (lua_isuserdata(l, -1)) {
		if (lua_getmetatable(l, -1)) {
#if LUA_VERSION_NUM > 502
			if (lua_getfield(l, -1, "_type") == LUA_TSTRING)
#else
			if (lua_getfield(l, -1, "_type"), lua_type(l, -1) == LUA_TSTRING)
#endif
			{
				if (!strcmp(lua_tostring(l, -1), "HttpHeaders")) {
					BBS2chProxyHttpHeaders *newHeaders = *((BBS2chProxyHttpHeaders **)lua_touserdata(l, -3));
					if (newHeaders != &headers) {
						headers = *newHeaders;
					}
					for (BBS2chProxyHttpHeaders::iterator it = headers.begin(); it != headers.end(); ++it) {
						log_printf(1, "Lua: Set request header \"%s\"\n", it->second.c_str());
					}
				}
			}
			lua_pop(l, 2);
		}
	}
	lua_pop(l, 1);
	lua_pushstring(l, "options");
	lua_rawget(l, -2);
	if (lua_istable(l, -1)) {
		lua_pushstring(l, "interface");
		lua_rawget(l, -2);
		if (lua_isstring(l, -1)) {
			_nic = std::string(lua_tostring(l, -1));
		}
		lua_pop(l, 1);
		lua_pushstring(l, "verbose");
		lua_rawget(l, -2);
		if (lua_isboolean(l, -1)) {
			_verbose = lua_toboolean(l, -1);
		}
		lua_pop(l, 1);
		lua_pushstring(l, "proxy");
		lua_rawget(l, -2);
		if (lua_isstring(l, -1)) {
			_forceProxy = std::string(lua_tostring(l, -1));
		}
		lua_pop(l, 1);
		lua_pushstring(l, "manageCookies");
		lua_rawget(l, -2);
		if (lua_isboolean(l, -1)) {
			if (curl_version_number >= 0x074d00) _manageCookies = lua_toboolean(l, -1);
		}
		lua_pop(l, 1);
	}
lua_end:
	lua_close(l);
#endif
}

void BBS2chProxy5chPoster::makeSignature(BBS2chProxyHttpHeaders &headers, BBS2chProxyFormData &body)
{
	bool isPink = _host.find("bbspink.com") != std::string::npos;
	bool shouldSign = appKey && (((api_mode & 2) && !isPink) || (api_mode & 4));
	bool shouldConvertBodyToUTF8 = (bbscgi_utf8 == 1 && shouldSign) || (bbscgi_utf8 == 2);
	_userAgentForRequest = headers.get("User-Agent");
	if (headers.has("X-MonaKey")) {
		_monaKeyForRequest = headers.get("X-MonaKey");
	}
	if (shouldConvertBodyToUTF8 && !headers.has("X-PostSig")) {
		if (convertBodyToUTF8(body)) {
			log_printf(1, "Converted request body to UTF-8: %s\n", body.toString().c_str());
		}
		else {
			log_printf(1, "Request body seems already to be UTF-8, will be sent without conversion\n");
		}
		std::string contentType = headers.get("Content-Type");
		std::transform(contentType.begin(), contentType.end(), contentType.begin(), tolower);
		if (contentType.find("charset=utf-8") == std::string::npos) {
			headers.set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
			log_printf(1, "Appended header \"Content-Type: application/x-www-form-urlencoded; charset=UTF-8\"\n");
		}
	}
	if (shouldSign && (!lua_script || !headers.has("X-PostSig"))) {
		if (!_userAgentForRequest.empty()) {
			_monaKeyForRequest = BBS2chProxyConnection::keyManager.getKey(_userAgentForRequest);
			appendPostSignature(body, _userAgentForRequest, _monaKeyForRequest, headers, false);
		} else {
			log_printf(0, "API: User-Agent muse be set explicitly to post with API.\n");
		}
	}
	if (!_monaKeyForRequest.empty()) {
		double wait = BBS2chProxyConnection::keyManager.secondsToWaitBeforePosting(_monaKeyForRequest);
		if (wait > 0) {
			log_printf(1, "Sleeping for %.1f seconds to avoid posting too fast...\n", wait);
#ifdef _WIN32
			Sleep(wait * 1e+3);
#else
			usleep(wait * 1e+6);
#endif
		}
	}
}

long BBS2chProxy5chPoster::post()
{
	CURL *curl = connectionDelegate->curl;
	long statusCode = 0;
	for (int run=0; run<2; run++) {
		BBS2chProxyHttpHeaders _headers = _requestHeaders;
		BBS2chProxyFormData _body = _requestBody;
		curl_slist *headersForCurl = NULL;
		std::string explicitAcceptEncoding;
		_verbose = 0;
		_status = 0;
		_monaKeyForRequest.clear();
		_nic.clear();
		_forceProxy.clear();
		_isFirstRun = run == 0;
		_responseHeaders.clear();
		_responseBody.clear();
		_isResponseChunked = false;
		_hasSetCookie = false;
		_manageCookies = manage_bbscgi_cookies;
		_useTransparentContentEncoding = false;
		_hasAcceptEncoding = false;
#ifdef USE_LUA
		if (lua_script) {
			runLuaScript(_headers, _body);
		}
#endif
		if (_confirmationHandlerMode) {
			bool modified = confirmationManager.modifyRequestBodyIfNeeded(*this, _body);
			if (modified) log_printf(1, "Looks like a response to a confirmation dialog. The request body has been modified to pass the confirmation.\n");
		}
		makeSignature(_headers, _body);
		if (_confirmationHandlerMode > 0 && _headers.has("Accept-Encoding")) {
			const std::string &value = _headers.get("Accept-Encoding");
			if (!value.empty()) {
				if (BBS2chProxyConnection::acceptEncodingChecker.isSupported(value)) {
					explicitAcceptEncoding = value;
					_useTransparentContentEncoding = true;
					_headers.remove("Accept-Encoding");
				}
				else if (_confirmationHandlerMode == 2 && run == 0) {
					_useTransparentContentEncoding = true;
					_headers.remove("Accept-Encoding");
				}
			}
			else _headers.remove("Accept-Encoding");
		}
		_hasAcceptEncoding = _headers.has("Accept-Encoding");
		headersForCurl = prepareCurlHandle(_headers, _body, headersForCurl);
		curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback_bbscgi);
		curl_easy_setopt(curl, CURLOPT_HEADERDATA, this);
		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback_bbscgi);
		curl_easy_setopt(curl, CURLOPT_WRITEDATA, this);
		if (_useTransparentContentEncoding) curl_easy_setopt(curl, CURLOPT_ENCODING, explicitAcceptEncoding.c_str());
		CURLcode res = curl_easy_perform(curl);
#if LIBCURL_VERSION_NUM >= 0x070e01 /* curl 7.14.1 or later */
		if (_manageCookies && _hasSetCookie) {
			appendCurlCookiesToJar(curl, BBS2chProxyConnection::keyManager.getCookieJar(_userAgentForRequest));
		}
#endif
		if (res != CURLE_OK) {
			if (res == CURLE_WRITE_ERROR && _status == 1) {
				log_printf(1, "MonaKey should be reset. Sending the same request automatically...\n");
				curl_easy_reset(curl);
				curl_slist_free_all(headersForCurl);
				continue;
			}
			else if (res == CURLE_WRITE_ERROR && _status == 2) {
				log_printf(1, "Acorn cookie is rotten. Sending the same request automatically...\n");
				curl_easy_reset(curl);
				curl_slist_free_all(headersForCurl);
				continue;
			}
			else {
				log_printf(0, "curl error: %s (%s)\n", curl_easy_strerror(res), _url.c_str());
				if (!_status) connectionDelegate->socketToClient->sendResponse(503, "Service Unavailable");
				statusCode = 503;
			}
		}
		else {
			if (_responseBody.size() > 0) {
				_responseBody.push_back('\0');
				PBBS2chProxyFormData body(new BBS2chProxyFormData());
				int appended = appendUnknownFieldFromResponseHtml(_responseBody, _requestBody, *body);
				if (appended) {
					body->append("time", _body.getEncoded("time"), true);
					confirmationManager.add(*this, body);
				}
				if (_confirmationHandlerMode == 2 && run == 0) {
					log_printf(1, "Detected a confirmation dialog. Will send the same request automatically after 1 second...\n");
					curl_easy_reset(curl);
					curl_slist_free_all(headersForCurl);
#ifdef _WIN32
					Sleep(1 * 1e+3);
#else
					usleep(1 * 1e+6);
#endif
					continue;
				}
			}
			if (_isResponseChunked) {
				connectionDelegate->socketToClient->writeString("0\r\n\r\n");
			}
			curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &statusCode);
		}
		curl_easy_reset(curl);
#if LIBCURL_VERSION_NUM >= 0x070e01 /* curl 7.14.1 or later */
		if (_manageCookies) {
			/* since curl_easy_reset() doesn't reset cookie engine, reset explicitly.
			   curl < 7.77.0 doesn't have a way to disable cookie engine,
			   so once enabled it will be enabled permanently. See https://github.com/curl/curl/issues/6889 */
			curl_easy_setopt(curl, CURLOPT_COOKIELIST, "ALL"); //force erase all cookies
			curl_easy_setopt(curl, CURLOPT_COOKIEFILE, NULL); //force disable cookie engine
		}
#endif
		curl_slist_free_all(headersForCurl);
		break;
	}
	return statusCode;
}

void BBS2chProxyTalkPoster::makeSignature(BBS2chProxyHttpHeaders &headers, BBS2chProxyFormData &body)
{
	bool shouldSign = appKey && (api_mode & 8);
	bool shouldConvertBodyToUTF8 = (bbscgi_utf8 == 1 && shouldSign) || (bbscgi_utf8 == 2);
	_userAgentForRequest = headers.get("User-Agent");
	if (headers.has("X-Write-Key")) {
		_monaKeyForRequest = headers.get("X-Write-Key");
	}
	if (shouldConvertBodyToUTF8 && !headers.has("X-Write-Token")) {
		if (convertBodyToUTF8(body)) {
			log_printf(1, "Converted request body to UTF-8: %s\n", body.toString().c_str());
		}
		else {
			log_printf(1, "Request body seems already to be UTF-8, will be sent without conversion\n");
		}
		std::string contentType = headers.get("Content-Type");
		std::transform(contentType.begin(), contentType.end(), contentType.begin(), tolower);
		if (contentType.find("charset=utf-8") == std::string::npos) {
			headers.set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
			log_printf(1, "Appended header \"Content-Type: application/x-www-form-urlencoded; charset=UTF-8\"\n");
		}
	}
	if (shouldSign && (!lua_script || !headers.has("X-Write-Token"))) {
		if (!_userAgentForRequest.empty()) {
			body.append("sid", BBS2chProxyConnection::auth.getSID());
			body.append("appkey", appKey);
			body.append("anonymous", body.has("subject") ? "true" : "false");
			_monaKeyForRequest = BBS2chProxyConnection::keyManager.getKey(_userAgentForRequest);
			appendPostSignature(body, _userAgentForRequest, _monaKeyForRequest, headers, true);
		} else {
			log_printf(0, "API: User-Agent muse be set explicitly to post with API.\n");
		}
	}
	if (!_monaKeyForRequest.empty()) {
		double wait = BBS2chProxyConnection::keyManager.secondsToWaitBeforePosting(_monaKeyForRequest);
		if (wait > 0) {
			log_printf(1, "Sleeping for %.1f seconds to avoid posting too fast...\n", wait);
#ifdef _WIN32
			Sleep(wait * 1e+3);
#else
			usleep(wait * 1e+6);
#endif
		}
	}
}

long BBS2chProxyTalkPoster::post()
{
	CURL *curl = connectionDelegate->curl;
	long statusCode = 0;
	for (int run=0; run<2; run++) {
		BBS2chProxyHttpHeaders _headers = _requestHeaders;
		BBS2chProxyFormData _body = _requestBody;
		curl_slist *headersForCurl = NULL;
		_verbose = 0;
		_nic.clear();
		_forceProxy.clear();
		_manageCookies = false;
#ifdef USE_LUA
		if (lua_script) {
			runLuaScript(_headers, _body);
			_manageCookies = false;
		}
#endif
		makeSignature(_headers, _body);
		headersForCurl = prepareCurlHandle(_headers, _body, headersForCurl);
		BBS2chProxyHttpHeaders receivedHeaders;
		std::vector<char> receivedBody;
		curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback_download);
		curl_easy_setopt(curl, CURLOPT_HEADERDATA, &receivedHeaders);
		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback_download);
		curl_easy_setopt(curl, CURLOPT_WRITEDATA, &receivedBody);
		curl_easy_setopt(curl, CURLOPT_ENCODING, "");
		CURLcode res = curl_easy_perform(curl);
		bool responseSent = false;
		if (res == CURLE_OK) curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &statusCode);
		if (receivedHeaders.hasNameAndValue("Content-Type", "application/json")) {
			receivedBody.push_back('\0');
			JSON_Value *json = json_parse_string(&receivedBody.front());
			if (json && json_type(json) == JSONObject) {
				JSON_Object *root = json_object(json);
				const char *error = json_object_dotget_string(root, "error.message");
				if (error) {
					log_printf(0, "Posting to bbs.cgi has been cancelled. Reason: %s\n", error);
					std::string out = "<html>\n<head>\n<title>ＥＲＲＯＲ！</title>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=Shift_JIS\">\n</head>\n<body bgcolor=\"#EFEFEF\">\n<font size=\"+1\" color=\"#FF0000\"><b>ERROR: ";
					out += error;
					out += "</b></font>\n</body>\n</html>";
					char *outSJIS = convertUTF8ToShiftJIS(out.c_str(), out.size());
					connectionDelegate->socketToClient->sendBasicHeaders(200, "OK");
					connectionDelegate->socketToClient->writeString("Content-Type: text/html; charset=Shift_JIS\r\n\r\n");
					if (outSJIS) {
						connectionDelegate->socketToClient->write(outSJIS, strlen(outSJIS));
						free(outSJIS);
					}
					statusCode = 200;
					responseSent = true;
				} else if (statusCode == 200) {
					int resNum = json_object_get_number(root, "commentNumber");
					std::string out = "<html lang=\"ja\">\n<head>\n<title>書きこみました。</title>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=Shift_JIS\">\n</head>\n<body>書きこみが終わりました。<br><br>\n画面を切り替えるまでしばらくお待ち下さい。<br><br>\n</body>\n</html>";
					char *outSJIS = convertUTF8ToShiftJIS(out.c_str(), out.size());
					connectionDelegate->socketToClient->sendBasicHeaders(200, "OK");
					if (resNum) {
						std::ostringstream ss;
						ss << "X-Resnum: " << resNum << "\r\n";
						connectionDelegate->socketToClient->writeString(ss.str());
					}
					connectionDelegate->socketToClient->writeString("Content-Type: text/html; charset=Shift_JIS\r\n\r\n");
					if (outSJIS) {
						connectionDelegate->socketToClient->write(outSJIS, strlen(outSJIS));
						free(outSJIS);
					}
					responseSent = true;
				}
			}
			if (json) json_value_free(json);
		}
		else if (receivedHeaders.has("X-Write-Key")) {
			BBS2chProxyConnection::keyManager.setKey(receivedHeaders.get("X-Write-Key"), _monaKeyForRequest, _userAgentForRequest, 0);
			const std::string &extendToken = receivedHeaders.get("X-Write-Key-Extend-Token");
			if (run == 0) {
				log_printf(1, "MonaKey has been updated. Sending the same request automatically...\n");
				if (!extendToken.empty()) {
					_requestHeaders.set("X-Write-Key-Extend-Token", extendToken);
				}
				curl_easy_reset(curl);
				curl_slist_free_all(headersForCurl);
				continue;
			} else if (!extendToken.empty()) {
				log_printf(0, "WARNING: you must send header \"X-Write-Key-Extend-Token: %s\" to complete posting.\n", extendToken.c_str());
			}
			std::string out = "<html><head><title>■ 書き込み確認 ■</title><meta http-equiv=\"Content-Type\" content=\"text/html; charset=Shift_JIS\"></head><body bgcolor=\"#EEEEEE\"><font size=\"+1\" color=\"#FF0000\"><b>書きこみ＆クッキー確認</b></font><br><br><b>投稿確認<br><b style=\"color: #F00; font-size: larger;\">この書き込みで本当にいいですか？<br>\n<form method=\"POST\" action=\"../test/bbs.cgi\" accept-charset=\"Shift_JIS\"><input type=hidden name=FROM value=><input type=hidden name=mail value=><input type=hidden name=MESSAGE value=><input type=hidden name=bbs value=><input type=hidden name=time value=><input type=hidden name=key value=><input type=submit value=\"上記全てを承諾して書き込む\" name=\"submit\"></form></body></html>";
			char *outSJIS = convertUTF8ToShiftJIS(out.c_str(), out.size());
			connectionDelegate->socketToClient->sendBasicHeaders(200, "OK");
			connectionDelegate->socketToClient->writeString("Content-Type: text/html; charset=Shift_JIS\r\n\r\n");
			if (outSJIS) {
				connectionDelegate->socketToClient->write(outSJIS, strlen(outSJIS));
				free(outSJIS);
			}
			statusCode = 200;
			responseSent = true;
		}
		if (!responseSent) {
			log_printf(5, "bbscgi response: %s\n", receivedHeaders.getStatusLine().c_str());
			for (BBS2chProxyHttpHeaders::iterator it = receivedHeaders.begin(); it != receivedHeaders.end(); ++it) {
				log_printf(5, "bbscgi response: %s\n", it->second.c_str());
			}
			receivedBody.push_back('\0');
			log_printf(5, "bbscgi response: %s\n", &receivedBody.front());
			connectionDelegate->socketToClient->sendResponse(503, "Service Unavailable");
			statusCode = 503;
		}
		curl_easy_reset(curl);
		curl_slist_free_all(headersForCurl);
		break;
	}
	return statusCode;
}

long BBS2chProxyTalkTo5chPoster::post()
{
	CURL *curl = connectionDelegate->curl;
	long statusCode = 0;
	std::string confirmationTime;
	for (int run=0; run<2; run++) {
		BBS2chProxyHttpHeaders _headers = _requestHeaders;
		BBS2chProxyFormData _body = _requestBody;
		curl_slist *headersForCurl = NULL;
		_verbose = 0;
		_status = 0;
		_monaKeyForRequest = "";
		_nic.clear();
		_forceProxy.clear();
		_manageCookies = manage_bbscgi_cookies;
		_responseBody.clear();
#ifdef USE_LUA
		if (lua_script) {
			runLuaScript(_headers, _body);
		}
#endif
		if (!confirmationTime.empty()) _body.set("time", confirmationTime);
		makeSignature(_headers, _body);
		headersForCurl = prepareCurlHandle(_headers, _body, headersForCurl);
		BBS2chProxyHttpHeaders receivedHeaders;
		curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback_download);
		curl_easy_setopt(curl, CURLOPT_HEADERDATA, &receivedHeaders);
		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback_download);
		curl_easy_setopt(curl, CURLOPT_WRITEDATA, &_responseBody);
		curl_easy_setopt(curl, CURLOPT_ENCODING, "");
		CURLcode res = curl_easy_perform(curl);
		if (res == CURLE_OK) {
			const std::string &errorHeader = receivedHeaders.get("X-Chx-Error");
			curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &statusCode);
			if (!errorHeader.empty() && (!strncmp(errorHeader.c_str(), "E3000 ", 6) || !strncmp(errorHeader.c_str(), "E3100 ", 6) || !strncmp(errorHeader.c_str(), "E4000 ", 6))) {
				std::string setCookie = "MonaTicket=; expires=Thu, 01-Jan-1970 00:00:00 GMT; path=/; domain=.5ch.net";
				receivedHeaders.add("Set-Cookie", setCookie);
#if LIBCURL_VERSION_NUM >= 0x070e01 /* curl 7.14.1 or later */
				if (_manageCookies) {
					curl_easy_setopt(curl, CURLOPT_COOKIELIST, setCookie.c_str());
				}
#endif
			}
			if (receivedHeaders.has("Set-Cookie")) {
#if LIBCURL_VERSION_NUM >= 0x070e01 /* curl 7.14.1 or later */
				if (_manageCookies) {
					appendCurlCookiesToJar(curl, BBS2chProxyConnection::keyManager.getCookieJar(_userAgentForRequest));
				}
#endif
				std::vector<std::string>& values = receivedHeaders.getEntry("Set-Cookie")->getValueList();
				for (std::vector<std::string>::iterator it = values.begin(); it != values.end(); it++) {
					std::string &value = *it;
					std::string lowercasedValue = value;
					std::transform(lowercasedValue.begin(), lowercasedValue.end(), lowercasedValue.begin(), tolower);
					size_t start = lowercasedValue.find("domain=");
					if (start == std::string::npos) continue;
					start += 7;
					size_t end = value.find(";", start);
					size_t pos = value.find("5ch.net", start);
					size_t domainLen = 7;
					if (pos == std::string::npos || (end != std::string::npos && pos >= end)) {
						pos = value.find("bbspink.com", start);
						domainLen = 11;
					}
					if (pos != std::string::npos && (end == std::string::npos || pos < end)) {
						if (pos == start || value[pos-1] == '.') {
							value.replace(start, pos+domainLen-start, ".talk-platform.com");
						}
					}
				}
			}
			if (statusCode == 200 && errorHeader.empty()) {
				std::ostringstream ss;
				ss << "{\"boardCode\":\"" << "5channel_" << _board << "\",\"threadNumber\":" << _thread << ",\"commentNumber\":" << receivedHeaders.get("X-Resnum") << "}";
				connectionDelegate->socketToClient->sendBasicHeaders(200, "OK");
				if (receivedHeaders.has("Set-Cookie")) connectionDelegate->socketToClient->writeString(receivedHeaders.getFull("Set-Cookie", true));
				connectionDelegate->socketToClient->writeString("Content-Type: application/json\r\n\r\n");
				connectionDelegate->socketToClient->writeString(ss.str());
			} else if (statusCode == 200) {
				if (run == 0 && _manageCookies && atoi(errorHeader.c_str()) == 1932) {
					log_printf(1, "Acorn cookie is rotten. Sending the same request automatically...\n");
					curl_easy_reset(curl);
					curl_slist_free_all(headersForCurl);
					continue;
				} else if (_confirmationHandlerMode && run == 0 && !strncmp(errorHeader.c_str(), "0000 Confirmation", 17)) {
					log_printf(1, "Detected a confirmation dialog. Will send the same request automatically after 1 second...\n");
					_responseBody.push_back('\0');
					appendUnknownFieldFromResponseHtml(_responseBody, _body, _requestBody);
					confirmationTime = _body.get("time");
					curl_easy_reset(curl);
					curl_slist_free_all(headersForCurl);
#ifdef _WIN32
					Sleep(1 * 1e+3);
#else
					usleep(1 * 1e+6);
#endif
					continue;
				}
				std::ostringstream ss;
				ss << "{\"error\":{\"message\":\"" << errorHeader << "\"}}";
				connectionDelegate->socketToClient->sendBasicHeaders(200, "OK");
				if (receivedHeaders.has("Set-Cookie")) connectionDelegate->socketToClient->writeString(receivedHeaders.getFull("Set-Cookie", true));
				connectionDelegate->socketToClient->writeString("Content-Type: application/json\r\n\r\n");
				connectionDelegate->socketToClient->writeString(ss.str());
			} else {
				connectionDelegate->socketToClient->sendResponse(503, "Service Unavailable");
				statusCode = 503;
			}
		} else {
			connectionDelegate->socketToClient->sendResponse(503, "Service Unavailable");
			statusCode = 503;
		}
		curl_easy_reset(curl);
		curl_slist_free_all(headersForCurl);
#if LIBCURL_VERSION_NUM >= 0x070e01 /* curl 7.14.1 or later */
		if (_manageCookies) {
			/* since curl_easy_reset() doesn't reset cookie engine, reset explicitly.
			   curl < 7.77.0 doesn't have a way to disable cookie engine,
			   so once enabled it will be enabled permanently. See https://github.com/curl/curl/issues/6889 */
			curl_easy_setopt(curl, CURLOPT_COOKIELIST, "ALL"); //force erase all cookies
			curl_easy_setopt(curl, CURLOPT_COOKIEFILE, NULL); //force disable cookie engine
		}
#endif
		break;
	}
	return statusCode;
}

std::string IBBS2chProxyPoster::identifier()
{
	std::string key = _host;
	key += "/";
	key += _board;
	key += "/";
	key += _thread;
	return key;
}

void BBS2chProxy5chPoster::ConfirmationManager::add(IBBS2chProxyPoster &poster, PBBS2chProxyFormData body)
{
	time_t now = time(NULL);
	pthread_mutex_lock(&_mutex);
	_cachedForms[poster.identifier()] = std::make_pair(now, body);
	_lastCached = now;
	pthread_mutex_unlock(&_mutex);
}

bool BBS2chProxy5chPoster::ConfirmationManager::modifyRequestBodyIfNeeded(IBBS2chProxyPoster &poster, BBS2chProxyFormData &body)
{
	bool ret = false;
	const std::string &key = poster.identifier();
	time_t now = time(NULL);
	pthread_mutex_lock(&_mutex);
	if (_lastCached && now - _lastCached > 120) {
		_cachedForms.clear();
		_lastCached = 0;
		pthread_mutex_unlock(&_mutex);
		return false;
	}
	std::map<std::string, std::pair<time_t, PBBS2chProxyFormData> >::iterator it = _cachedForms.find(key);
	if (it != _cachedForms.end()) {
		if (now - it->second.first <= 120) {
			PBBS2chProxyFormData cachedBody = it->second.second;
			for (BBS2chProxyFormData::iterator it = cachedBody->begin(); it != cachedBody->end(); ++it) {
				body.set(it->first, it->second.getEncoded(), true);
			}
			ret = true;
		}
		_cachedForms.erase(it);
	}
	pthread_mutex_unlock(&_mutex);
	return ret;
}
